package orm

import (
	"context"
	"errors"
	"runtime"
	"strings"

	"gorm.io/gorm"
)

func DBPanic(ctx context.Context, tx *gorm.DB) (err error) {
	defer func() {
		if r := recover(); r != nil {
			tx.Rollback()
			buf := make([]byte, 64<<10)
			n := runtime.Stack(buf, false)
			buf = buf[:n]
			errMsg := "a panic error occurred during database operation"
			tx.Logger.Error(ctx, errMsg, r, buf)
			err = errors.New(errMsg)
		}
	}()
	return
}

func DBRollback(ctx context.Context, tx *gorm.DB) error {
	if err := tx.Rollback().Error; err != nil {
		tx.Logger.Error(ctx, "database rollback error", err)
		return err
	}
	return nil
}

func DBCommit(ctx context.Context, tx *gorm.DB) error {
	if err := tx.Commit().Error; err != nil {
		tx.Logger.Error(ctx, "database commit error", err)
		DBRollback(ctx, tx)
		return err
	}
	return nil
}

func DBAssociate(ctx context.Context, db *gorm.DB, om any, upmap map[string]any) error {
	for k, v := range upmap {
		if err := db.Model(om).Association(k).Replace(v); err != nil {
			return err
		}
	}
	return nil
}

func DBCreate(ctx context.Context, db *gorm.DB, model, value any) error {
	return db.Model(model).Create(value).Error
}

func DBUpdate(ctx context.Context, db *gorm.DB, im, om any, upmap map[string]any, conds ...any) error {
	if len(conds) == 0 {
		return gorm.ErrMissingWhereClause
	}
	if len(upmap) == 0 {
		return db.Model(om).Where(conds[0], conds[1:]...).Updates(im).Error
	}
	tx := db.Begin()
	if tx.Error != nil {
		tx.Logger.Error(ctx, "faild to begin transaction for database", tx.Error)
		return tx.Error
	}
	defer DBPanic(ctx, tx)
	if err := tx.Model(om).Where(conds[0], conds[1:]...).Updates(im).Error; err != nil {
		DBRollback(ctx, tx)
		return err
	}
	if err := DBAssociate(ctx, tx, om, upmap); err != nil {
		DBRollback(ctx, tx)
		return err
	}
	return DBCommit(ctx, tx)
}

func DBDelete(ctx context.Context, db *gorm.DB, model any, conds ...any) error {
	if len(conds) == 0 {
		return gorm.ErrMissingWhereClause
	}
	return db.Delete(model, conds...).Error
}

func DBFind(ctx context.Context, db *gorm.DB, preloads []string, m any, conds ...any) error {
	for _, preload := range preloads {
		db = db.Preload(preload)
	}
	return db.First(m, conds...).Error
}

type QueryParams struct {
	Preloads []string
	Query    map[string]any
	OrderBy  []string
	Limit    int
	Offset   int
	IsCount  bool
}

func DBList(ctx context.Context, db *gorm.DB, model, value any, query QueryParams) (int64, error) {
	mdb := db.Model(model)
	for _, preload := range query.Preloads {
		mdb = mdb.Preload(preload)
	}
	for k, v := range query.Query {
		mdb = mdb.Where(k, v)
	}
	var count int64 = 0
	if query.IsCount {
		mdb = mdb.Count(&count)
	}
	orderByStr := strings.Join(query.OrderBy, ",")
	if orderByStr != "" {
		mdb = mdb.Order(orderByStr)
	}
	if query.Limit > 0 {
		mdb = mdb.Limit(query.Limit)
	}
	if query.Offset > 0 {
		mdb = mdb.Offset(query.Offset)
	}
	result := mdb.Find(value)
	if result.Error != nil {
		return 0, result.Error
	}
	if !query.IsCount {
		count = result.RowsAffected
	}
	return count, nil
}
